﻿//-----------------------------------------------------------------------------
// FILE:	    DebugHelper.cs
// CONTRIBUTOR: Jeff Lill
// COPYRIGHT:   Copyright (c) 2021 by neonFORGE, LLC.  All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Threading;

using EnvDTE;
using EnvDTE80;

using Neon.Common;
using Neon.IO;
using Neon.Windows;

using Newtonsoft.Json.Linq;

using Task = System.Threading.Tasks.Task;

namespace RaspberryDebugger
{
    /// <summary>
    /// Remote debugger related utilties.
    /// </summary>
    internal static class DebugHelper
    {
        private const string caption = "调试器安装失败";
        /// <summary>
        /// 提示，确保已安装本机Windows OpenSSH客户端  
        ///如果需要安装它的用户。  
        /// </summary>
        /// <returns><c>true</c> if OpenSSH is installed.</returns>
        public static async Task<bool> EnsureOpenSshAsync()
        {
            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            Log.Info($"      超级np的小虎 \r\n Gitee:https://gitee.com/hejiale010426 \r\n支持小虎的请来个star");
            Log.Info("检查本机Windows OpenSSH客户端");
            var openSshPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "OpenSSH", "ssh.exe");

            if(!File.Exists(openSshPath))
            {
                Log.WriteLine("Raspberry调试需要本机的Windows OpenSSH客户端。 看到这个:  ");
                Log.WriteLine("https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540");

                var button = MessageBox.Show(
                    "Raspberry调试需要Windows OpenSSH客户端  .\r\n\r\n您想现在安装(需要重新启动)吗?  ",
                    "Windows OpenSSH客户端必备",
                    MessageBoxButtons.YesNo,
                    MessageBoxIcon.Question,
                    MessageBoxDefaultButton.Button2);

                if(button != DialogResult.Yes)
                {
                    return false;
                }

                // Install via Powershell: https://techcommunity.microsoft.com/t5/itops-talk-blog/installing-and-configuring-openssh-on-windows-server-2019/ba-p/309540

                await PackageHelper.ExecuteWithProgressAsync("安装OpenSSH客户",
                    async () =>
                    {
                        using(var powershell = new PowerShell())
                        {
                            Log.Info("安装OpenSSH");
                            Log.Info(powershell.Execute("Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"));
                        }

                        await Task.CompletedTask;
                    });

                MessageBox.Show(
                    "重启Windows系统，完成OpenSSH客户端安装。  ",
                    "Restart Required",
                    MessageBoxButtons.OK);

                return false;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        ///尝试定位要调试的启动项目，确保它是  
        ///可以调试树莓。  
        /// </summary>
        /// <param name="dte">The IDE.</param>
        /// <returns>The target project or <c>null</c> if there isn't a startup project or it wasn't eligible.</returns>
        public static Project GetTargetProject(DTE2 dte)
        {
            ThreadHelper.ThrowIfNotOnUIThread();

            // Identify the current startup project (if any).

            if(dte.Solution == null)
            {
                MessageBox.Show(
                    "Please open a Visual Studio solution.",
                    "Solution Required",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);

                return null;
            }

            var project = PackageHelper.GetStartupProject(dte.Solution);

            if(project == null)
            {
                MessageBox.Show(
                    "Please select a startup project for your solution.",
                    "Startup Project Required",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Information);

                return null;
            }

            //我们需要捕获相关的项目属性，而我们仍然  
            //在UI线程上，所以我们会在后台线程上有它们。  

            var projectProperties = ProjectProperties.CopyFrom(dte.Solution, project);

            if(!projectProperties.IsNetCore)
            {
                MessageBox.Show(
                    "只有.net Core 3.1或.net 5 + 6项目支持Raspberry调试。  ",
                    "Invalid Project Type",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            if(!projectProperties.IsExecutable)
            {
                MessageBox.Show(
                    "只有生成可执行程序的项目类型才支持Raspberry调试。  ",
                    "Invalid Project Type",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            if(string.IsNullOrEmpty(projectProperties.SdkVersion))
            {
                MessageBox.Show(
                    "The .NET Core SDK 无法识别版本。",
                    "Invalid Project Type",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            var sdkVersion = Version.Parse(projectProperties.SdkVersion);

            if(!projectProperties.IsSupportedSdkVersion)
            {
                MessageBox.Show(
                    $"The .NET Core SDK [{sdkVersion}] is not currently supported.  Only .NET Core versions [v3.1] or later will ever be supported\r\n\r\nNote that we currently support only offical SDKs (not previews or release candidates) and we check for new .NET Core SDKs every week or two.  Submit an issue if you really need support for a new SDK ASAP:\r\n\t\nhttps://github.com/nforgeio/RaspberryDebugger/issues",
                    "SDK Not Supported",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            if(projectProperties.AssemblyName.Contains(' '))
            {
                MessageBox.Show(
                    $"Your assembly name [{projectProperties.AssemblyName}] includes a space.  This isn't supported.",
                    "Unsupported Assembly Name",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }

            return project;
        }

        /// <summary>
        ///构建和发布一个项目在本地准备它被上传到树莓派。 这个方法  
        ///将在失败时向用户显示一个错误消息框  
        /// </summary>
        /// <param name="dte"></param>
        /// <param name="solution"></param>
        /// <param name="project"></param>
        /// <param name="projectProperties"></param>
        /// <returns></returns>
        public static async Task<bool> PublishProjectWithUIAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(dte != null, nameof(dte));
            Covenant.Requires<ArgumentNullException>(solution != null, nameof(solution));
            Covenant.Requires<ArgumentNullException>(project != null, nameof(project));
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            if(!await PublishProjectAsync(dte, solution, project, projectProperties))
            {
                MessageBox.Show(
                    "[dotnet publish]项目失败。查看输出/调试面板了解更多细节。",
                    "Publish Failed",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return false;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        ///构建和发布一个项目在本地准备它被上传到树莓派。 这个方法  
        ///失败时不向用户显示错误提示框  
        /// </summary>
        /// <param name="dte">The DTE.</param>
        /// <param name="solution">The solution.</param>
        /// <param name="project">The project.</param>
        /// <param name="projectProperties">The project properties.</param>
        /// <returns><c>true</c> on success.</returns>
        public static async Task<bool> PublishProjectAsync(DTE2 dte, Solution solution, Project project, ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(dte != null, nameof(dte));
            Covenant.Requires<ArgumentNullException>(solution != null, nameof(solution));
            Covenant.Requires<ArgumentNullException>(project != null, nameof(project));
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            //在VS的上下文中构建项目，以确保所有的更改  
            //文件被保存，所有的依赖会首先被构建。 然后我们将  
            //验证在继续之前没有错误。  

            await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

            //确保项目被Visual Studio完全加载。 我看过  
            //当VS还在加载时构建或发布项目时随机崩溃  
            // 项目。  

            var projectGuid = projectProperties.Guid;
            var solutionService4 = (IVsSolution4) await RaspberryDebuggerPackage.Instance.GetServiceAsync(typeof(SVsSolution));

            if(solutionService4 == null)
            {
                Covenant.Assert(solutionService4 != null, $"Service [{typeof(SVsSolution).Name}] is not available.");
            }

            //构建项目以确保没有编译时错误。  

            Log.Info($"Building: {projectProperties.FullPath}");

            solution.SolutionBuild.BuildProject(solution.SolutionBuild.ActiveConfiguration.Name, project.UniqueName, WaitForBuildToFinish: true);

            var errorList = dte.ToolWindows.ErrorList.ErrorItems;

            if(errorList.Count > 0)
            {
                for(int i = 1;i <= errorList.Count;i++)
                {
                    var error = errorList.Item(i);

                    Log.Error($"{error.FileName}({error.Line},{error.Column}: {error.Description})");
                }

                Log.Error($"Build failed: [{errorList.Count}] errors");
                Log.Error($"See the Build/Output panel for more information");
                return false;
            }

            Log.Info("Build succeeded");

            //发布项目，所有需要的二进制文件和资产结束  
            //在输出文件夹。  
            //  
            //注意，我们只转发了几个标准  
            //环境变量，因为Visual Studio似乎可以交流  
            //使用环境变量和  
            //当我们调用下面的[dotnet]时，这些会导致冲突  
            //发布项目。  

            Log.Info($"Publishing: {projectProperties.FullPath}");

            await Task.Yield();

            var allowedVariableNames =
@"
ALLUSERSPROFILE
APPDATA
architecture
architecture_bits
CommonProgramFiles
CommonProgramFiles(x86)
CommonProgramW6432
COMPUTERNAME
ComSpec
DOTNETPATH
DOTNET_CLI_TELEMETRY_OPTOUT
DriverData
HOME
HOMEDRIVE
HOMEPATH
LOCALAPPDATA
NUMBER_OF_PROCESSORS
OS
Path
PATHEXT
POWERSHELL_DISTRIBUTION_CHANNEL
PROCESSOR_ARCHITECTURE
PROCESSOR_IDENTIFIER
PROCESSOR_LEVEL
PROCESSOR_REVISION
ProgramData
ProgramFiles
ProgramFiles(x86)
ProgramW6432
PUBLIC
SystemDrive
SystemRoot
TEMP
USERDOMAIN
USERDOMAIN_ROAMINGPROFILE
USERNAME
USERPROFILE
windir
";
            var allowedVariables = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
            var environmentVariables = new Dictionary<string, string>();

            using(var reader = new StringReader(allowedVariableNames))
            {
                foreach(var line in reader.Lines())
                {
                    if(string.IsNullOrWhiteSpace(line))
                    {
                        continue;
                    }

                    allowedVariables.Add(line.Trim());
                }
            }

            foreach(string variable in Environment.GetEnvironmentVariables().Keys)
            {
                if(allowedVariables.Contains(variable))
                {
                    environmentVariables[variable] = Environment.GetEnvironmentVariable(variable);
                }
            }

            try
            {
                var response = await NeonHelper.ExecuteCaptureAsync(
                    "dotnet",
                    new object[]
                    {
                        "publish",
                        "--configuration", projectProperties.Configuration,
                        //"--runtime", projectProperties.Runtime,
                        "--runtime", "linux-x64",
                        "--no-self-contained",
                        "--output", projectProperties.PublishFolder,
                        projectProperties.FullPath
                    },
                    environmentVariables: environmentVariables);

                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                if(response.ExitCode == 0)
                {
                    Log.Error("Publish succeeded");
                    return true;
                }

                Log.Error($"Publish failed: ExitCode={response.ExitCode}");
                Log.WriteLine(response.AllText);

                return false;
            }
            catch(Exception e)
            {
                Log.Error(NeonHelper.ExceptionError(e));

                return false;
            }
        }

        /// <summary>
        ///将我们从项目属性(如果有)获得的调试连接名称映射到  
        ///我们的树莓连接之一。 如果没有指定名称，我们将  
        ///使用默认连接或提示用户创建连接。  
        ///如果指定了一个连接但不存在，则会显示一个错误。  
        /// </summary>
        /// <param name="projectProperties">The project properties.</param>
        /// <returns>The connection or <c>null</c> when one couldn't be located.</returns>
        public static ConnectionInfo GetDebugConnectionInfo(ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            var existingConnections = PackageHelper.ReadConnections();
            var connectionInfo = (ConnectionInfo) null;

            if(string.IsNullOrEmpty(projectProperties.DebugConnectionName))
            {
                connectionInfo = existingConnections.SingleOrDefault(info => info.IsDefault);

                if(connectionInfo == null)
                {
                    if(MessageBoxEx.Show(
                        $"Raspberry connection information required.  Would you like to create a connection now?",
                        "Raspberry Connection Required",
                        MessageBoxButtons.YesNo,
                        MessageBoxIcon.Error,
                        MessageBoxDefaultButton.Button1) == DialogResult.No)
                    {
                        return null;
                    }

                    connectionInfo = new ConnectionInfo();

                    var connectionDialog = new ConnectionDialog(connectionInfo, edit: false, existingConnections: existingConnections);

                    if(connectionDialog.ShowDialog() == DialogResult.OK)
                    {
                        existingConnections.Add(connectionInfo);
                        PackageHelper.WriteConnections(existingConnections, disableLogging: true);
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            else
            {
                connectionInfo = existingConnections.SingleOrDefault(info => info.Name.Equals(projectProperties.DebugConnectionName, StringComparison.InvariantCultureIgnoreCase));

                if(connectionInfo == null)
                {
                    MessageBoxEx.Show(
                        $"The [{projectProperties.DebugConnectionName}] Raspberry connection does not exist.\r\n\r\nPlease add the connection via:\r\n\r\nTools/Options/Raspberry Debugger",
                        "Cannot Find Raspberry Connection",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Error);

                    return null;
                }
            }

            return connectionInfo;
        }

        /// <summary>
        ///确定项目的目标.net SDK。  
        /// </summary>
        /// <param name="projectProperties">The project properties.</param>
        /// <returns>The target <see cref="SDK"/> or <c>null</c> when one could not be located.</returns>
        public static Sdk GetTargetSdk(ProjectProperties projectProperties)
        {
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));

            //我们只是要返回一个匹配的SDK从目录(如果它存在)。  
            //注意，有不止一个匹配一个独立的SDK或一个发货  
            //使用Visual Studio。 如果可能的话，我们更倾向于单机。  

            var sdkItem = PackageHelper.SdkGoodCatalog.Items.SingleOrDefault(item => item.Version == projectProperties.SdkVersion &&
                                                                                 item.IsStandalone &&
                                                                                 item.Architecture == SdkArchitecture.ARM32);
            if(sdkItem == null)
            {
                //寻找一个Visual Studio SDK代替。

                sdkItem = PackageHelper.SdkGoodCatalog.Items.SingleOrDefault(item => item.Version == projectProperties.SdkVersion &&
                                                                                 item.Architecture == SdkArchitecture.ARM32);
            }
            if(sdkItem == null)
            {
                //寻找一个Visual Studio SDK代替。
                sdkItem = PackageHelper.SdkGoodCatalog.Items.SingleOrDefault(item => item.Version == projectProperties.SdkVersion &&
                                                                                 item.Architecture == SdkArchitecture.x64);
            }

            if(sdkItem == null)
            {
                MessageBoxEx.Show(
                    $".NET Core SDK [v{projectProperties.SdkVersion}] 是未知的树莓调试器。 请提交一个问题 at:\r\n\r\n{PackageHelper.GitHubIssuesUri}",
                    "未知 .NET Core SDK",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                return null;
            }
            else
            {
                return new Sdk(sdkItem.Name, sdkItem.Version);
            }
        }

        /// <summary>
        ///连接树莓，并确保树莓已经  
        ///目标SDK， <b>vsdbg</b>安装，也处理项目的上传  
        ///二进制文件。  
        /// </summary>
        /// <param name="connectionInfo">The connection info.</param>
        /// <param name="targetSdk">The target SDK.</param>
        /// <param name="projectProperties">The project properties.</param>
        /// <param name="projectSettings">The project's Raspberry debug settings.</param>
        /// <returns>The <see cref="Connection"/> or <c>null</c> if there was an error.</returns>
        public static async Task<Connection> InitializeConnectionAsync(ConnectionInfo connectionInfo, Sdk targetSdk, ProjectProperties projectProperties, ProjectSettings projectSettings)
        {
            Covenant.Requires<ArgumentNullException>(connectionInfo != null, nameof(connectionInfo));
            Covenant.Requires<ArgumentNullException>(targetSdk != null, nameof(targetSdk));
            Covenant.Requires<ArgumentNullException>(projectProperties != null, nameof(projectProperties));
            Covenant.Requires<ArgumentNullException>(projectSettings != null, nameof(projectSettings));

            var connection = await Connection.ConnectAsync(connectionInfo, projectSettings: projectSettings);

            var architecture = new string[]{ "aarch64",
            "x86_64"};

            // 在这里写兼容架构
            if(!architecture.Any(x => connection.PiStatus.Architecture.Equals(x)) &&
                connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi 3 Model") &&
                !connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi 4 Model") &&
                !connection.PiStatus.RaspberryModel.StartsWith("Raspberry Pi Zero 2"))
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"Your [{connection.PiStatus.RaspberryModel}]不支持。" +
                    $" .NET Core requires a Raspberry Model 3 or 4 or Pi Zero 2.",
                    $"设备不支持",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            //请确保已安装SDK。

            if(!await connection.InstallSdkAsync(targetSdk.Version, connection.PiStatus.Architecture))
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"无法安装。net SDK [v{targetSdk.Version}]上的树莓。查看调试输出以获取更多详细信息。",
                    caption,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            // Ensure that the debugger is installed.

            if(!await connection.InstallDebuggerAsync())
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"无法在树莓上安装VSDBG调试器。查看调试输出以获取更多详细信息调试器安装失败",
                    caption,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            // Upload the program binaries.

            if(!await connection.UploadProgramAsync(projectProperties.Name, projectProperties.AssemblyName, projectProperties.PublishFolder))
            {
                await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

                MessageBoxEx.Show(
                    $"无法上传程序二进制文件到设备。查看调试输出以获取更多详细信息。",
                    caption,
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);

                connection.Dispose();
                return null;
            }

            return connection;
        }
    }
}

